<template>
  <div class="cp-earth"
    @mousedown="onMouseDown"
    @mousemove="onMouseMove"
    @mouseup="onMouseUp"
    @mouseover="onMouseOver"
    @mouseout="onMouseOut"
    @mousewheel="onMouseWheel">
  </div>
</template>

<script>
const THREE = require('three');
import {randint} from 'utils/random'

/*=======================*\
|          常量定义        |
\*=======================*/
// 着色器
var Shaders = {
  earth: {
    vertexShader: `
      varying vec3 vNormal;
      varying vec2 vUv;
      void main() {
        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
        vNormal = normalize( normalMatrix * normal );
        vUv = uv;
      }
    `,
    fragmentShader: `
      uniform sampler2D texture;
      varying vec3 vNormal;
      varying vec2 vUv;
      void main() {
        vec3 diffuse = texture2D( texture, vUv ).xyz;
        float intensity = 1.05 - dot( vNormal, vec3( 0.0, 0.0, 1.0 ) );
        vec3 atmosphere = vec3( 1.0, 1.0, 1.0 ) * pow( intensity, 3.0 );
        gl_FragColor = vec4( diffuse + atmosphere, 1.0 );
      }
    `
  },
  atmosphere: {
    vertexShader: `
      varying vec3 vNormal;
      void main() {
        vNormal = normalize( normalMatrix * normal );
        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
      }
    `,
    fragmentShader: `
      varying vec3 vNormal;
      void main() {
        float intensity = pow( 0.8 - dot( vNormal, vec3( 0, 0, 1.0 ) ), 12.0 );
        gl_FragColor = vec4( 1.0, 1.0, 1.0, 1.0 ) * intensity;
      }
    `
  },
  glow: {
    vertexShader: `
      uniform vec3 viewVector;
      uniform float c;
      uniform float p;
      varying float intensity;
      void main() {
        vec3 vNormal = normalize( normalMatrix * normal );
        vec3 vNormel = normalize( normalMatrix * viewVector );
        intensity = pow(abs(c - dot(vNormal, vNormel)), p );
        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
      }
    `,
    fragmentShader: `
      uniform vec3 glowColor;
      varying float intensity;
      void main() {
        vec3 glow = glowColor * intensity;
        gl_FragColor = vec4( glow, 1.0 );
      }
    `
  }
};

/*=======================*\
|         工具函数         |
\*=======================*/
//经纬度转换为三维直角坐标
function conv(lat, lng){
  var phi = (90 - lat) * Math.PI / 180;
  var theta = (180 - lng) * Math.PI / 180;
  return new THREE.Vector3(
    200 * Math.sin(phi) * Math.cos(theta),
    200 * Math.cos(phi),
    200 * Math.sin(phi) * Math.sin(theta)
  )
}

/*=======================*\
|          初始化          |
\*=======================*/
var width  = window.innerWidth,
  height = window.innerHeight;
var loader = new THREE.TextureLoader();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(30, width / height, 1, 10000);
camera.position.z = 1000;
var renderer = new THREE.WebGLRenderer({antialias: true, alpha: true});
renderer.setSize(width, height);
var glowMaterial = new THREE.ShaderMaterial({
  uniforms: {
    "c":   { type: "f", value: 0.2 },
    "p":   { type: "f", value: 1.4 },
    glowColor: { type: "c", value: new THREE.Color(0xffff00) },
    viewVector: { type: "v3", value: camera.position }
  },
  vertexShader:   Shaders.glow.vertexShader,
  fragmentShader: Shaders.glow.fragmentShader,
  side: THREE.FrontSide,
  blending: THREE.AdditiveBlending,
  transparent: true
})

/*=======================*\
|          地球            |
\*=======================*/
var earthGeo = new THREE.SphereGeometry(200, 40, 40)
var earthmaterial = new THREE.ShaderMaterial({
  uniforms: {
    texture: {type: 't', value: loader.load(require('./img/earth-world.jpg'))}
  },
  vertexShader: Shaders.earth.vertexShader,
  fragmentShader: Shaders.earth.fragmentShader
})
var earth = new THREE.Mesh(
  earthGeo.clone(),
  earthmaterial
);
earth.rotation.y = Math.PI;
scene.add(earth)

/*=======================*\
|          大气层          |
\*=======================*/
var atmospherematerial = new THREE.ShaderMaterial({
  uniforms: {},
  vertexShader: Shaders.atmosphere.vertexShader,
  fragmentShader: Shaders.atmosphere.fragmentShader,
  side: THREE.BackSide,
  blending: THREE.AdditiveBlending,
  transparent: true
});
var atmosphere = new THREE.Mesh(
  earthGeo.clone(),
  atmospherematerial
);
atmosphere.scale.multiplyScalar(1.08)
scene.add(atmosphere);

/*=======================*\
|           星空           |
\*=======================*/
var stars = new THREE.Mesh(
  new THREE.SphereGeometry(650, 64, 64),
  new THREE.MeshBasicMaterial({
    map:  loader.load(require('./img/earth-galaxy.jpg')),
    side: THREE.BackSide
  })
);
scene.add(stars);

/*=======================*\
|         柱形图           |
\*=======================*/
var pointGeo = new THREE.CylinderGeometry(1, 1, 1, 16)
pointGeo.rotateX(Math.PI / 2)
pointGeo.translate(0,0,-0.5)
function addHistogramPoint(lat, lng, size){
  var height = 36 * size
  height < 1 && (height = 1)
  var hue = 131 - Math.floor(size * (131 - 30))
  var point = new THREE.Mesh(
    pointGeo,
    new THREE.MeshBasicMaterial({color: new THREE.Color(`hsl(${hue}, 88%, 63%)`)})
  )
  point.position.copy(conv(lat, lng))
  point.lookAt(earth.position)
  point.scale.z = height
  scene.add(point)
}

/*=======================*\
|         曲线和球         |
\*=======================*/
var waveGeo = new THREE.CylinderGeometry(1, 1, 0.1, 32)
waveGeo.rotateX(Math.PI / 2)
waveGeo.translate(0,0,-0.05)
class ShootStar{
  constructor(){
    this.position = 0 //当前点的位置
    this.count = 0    //总点数
    this.color = randint(0xffffff)
    this.vertices = []
    this.glows = []
    this.length = 60 //拖尾长度
    let material = glowMaterial.clone()
    material.uniforms.glowColor.value = new THREE.Color(this.color)
    this.glow = new THREE.Mesh(
      new THREE.SphereGeometry(2, 8, 8),
      material
    )
    this.wave = new THREE.Mesh(
      waveGeo,
      new THREE.MeshBasicMaterial({color: this.color})
    )
  }
  addCurve(start, end){
    var startToEnd = start.clone().sub(end)
    var startToEndLength = startToEnd.length()
    var startToEndHalf = startToEndLength / 2
    var startToEndNormal = startToEnd.clone().normalize()
    var mid = start.clone().lerp(end, 0.5).normalize().multiplyScalar(randint(240, 260))
    var midStartCtl = mid.clone().add(startToEndNormal.clone().multiplyScalar(startToEndHalf))
    var midEndCtl = mid.clone().add(startToEndNormal.clone().multiplyScalar(-startToEndHalf))
    var curveA = new THREE.CubicBezierCurve3(start, start, midStartCtl, mid)
    var curveB = new THREE.CubicBezierCurve3(mid, midEndCtl, end, end)
    var vertexCount = Math.floor(startToEndLength * 1 + 3)
    vertexCount * 2 - 1 < this.length && (vertexCount = Math.ceil(this.length / 2) + 1)
    this.vertices = curveA.getPoints(vertexCount).slice(0, vertexCount - 1)
    this.vertices = this.vertices.concat(curveB.getPoints(vertexCount))
    this.count = this.vertices.length
  }
  addBall(){
    for(let i = 0; i < this.length; i++){
      let outGlow = this.glow.clone()
      let t = (this.length - i) / this.length
      t < 0.01 && (t = 0.01)
      outGlow.scale.set(t, t, t)
      outGlow.position.copy(this.vertices[this.length - i])
      this.glows.push(outGlow)
      scene.add(outGlow)
    }
  }
  updateBall(){
    for(let i = 0; i < this.length; i++){
      let j = this.position - i
      j < 0 && (j += this.count)
      this.glows[i].position.copy(this.vertices[j])
    }
  }
  addWave(end){
    this.wave.position.copy(end)
    this.wave.lookAt(earth.position)
    this.wave.material.transparent = true
    scene.add(this.wave)
  }
  updateWave(){
    if(this.position < 60){
      this.wave.material.opacity = 1 - this.position / 60
      let size = 16 * this.position / 60
      size < 0.01 && (size = 0.01)
      this.wave.scale.set(size, size, 1)
      this.position === 0 && (this.wave.visible = true)
    }else if(this.position === 60){
      this.wave.visible = false
    }
  }
  init(lat0, lng0, lat1, lng1){
    var start = conv(lat0, lng0)
    var end = conv(lat1, lng1)
    this.addCurve(start, end)
    this.addBall()
    this.addWave(end)
  }
  update(){
    this.position += 3
    this.position >= this.count && (this.position = 0)
    this.updateBall()
    this.updateWave()
  }
}



/*=======================*\
|        加载假数据        |
\*=======================*/
var shootStars = []
fetch('./static/histogramdata.json')
.then(res => res.json())
.then(res => {
  let pLat, pLng
  for(let i = 0; i < 80; i++){
    let lat = res[i][0]
    let lng = res[i][1]
    addHistogramPoint(lat, lng, randint(1, 100) / 100)
    if(i % 2 == 1){
      let star = new ShootStar()
      star.init(pLat, pLng, lat, lng)
      shootStars.push(star)
    }else{
      pLat = lat
      pLng = lng
    }
  }
})

/*=======================*\
|    事件绑定，动画循环     |
\*=======================*/
var target = {x: 0, y: 0};
var targetOnDown = {x: 0, y: 0};
var mouse = {x: 0, y: 0};
var mouseOnDown = {x: 0, y: 0};
var rotation = {x: 0, y: 0};
var distance = 1000, distanceTarget = 1000;
var mousedown = false;
var overRenderer = false;
var PI_HALF = Math.PI / 2;

function zoom(speed) {
  distanceTarget -= speed;
  distanceTarget = distanceTarget > 1200 ? 1200 : distanceTarget;
  distanceTarget = distanceTarget < 400 ? 400 : distanceTarget;
}

function onMouseDown(event) {
  event.preventDefault();
  mouseOnDown.x = - event.clientX;
  mouseOnDown.y = event.clientY;
  targetOnDown.x = target.x;
  targetOnDown.y = target.y;
  mousedown = true;
}

function onMouseMove(event) {
  if(!mousedown) return;
  var zoomDamp = distance / 1000;
  mouse.x = -event.clientX;
  mouse.y = event.clientY;
  target.x = targetOnDown.x + (mouse.x - mouseOnDown.x) * 0.005 * zoomDamp;
  target.y = targetOnDown.y + (mouse.y - mouseOnDown.y) * 0.005 * zoomDamp;
  target.y = target.y > PI_HALF ? PI_HALF : target.y;
  target.y = target.y < - PI_HALF ? - PI_HALF : target.y;
}

function onMouseUp(event) {
  mousedown = false;
}

function onMouseOver(event) {
  overRenderer = true;
}

function onMouseOut(event) {
  mousedown = false;
  overRenderer = false;
}

function onMouseWheel(event) {
  event.preventDefault();
  if (overRenderer) {
    zoom(event.wheelDeltaY * 0.3);
  }
}

var clock = new THREE.Clock(true);
var tick = 0
function render(delta) {
  target.x -= 0.002
  rotation.x += (target.x - rotation.x) * 0.1
  rotation.y += (target.y - rotation.y) * 0.1
  distance += (distanceTarget - distance) * 0.3
  camera.position.x = distance * Math.sin(rotation.x) * Math.cos(rotation.y)
  camera.position.y = distance * Math.sin(rotation.y)
  camera.position.z = distance * Math.cos(rotation.x) * Math.cos(rotation.y)
  camera.lookAt(earth.position)
  for(let item of shootStars){
    item.update()
  }
  renderer.render(scene, camera)
  requestAnimationFrame(render)
}
render()

/*=======================*\
|         组件定义         |
\*=======================*/
export default {
  name: 'earth',
  mounted() {
    this.$el.appendChild(renderer.domElement);
  },
  methods: {
    onMouseDown,
    onMouseMove,
    onMouseUp,
    onMouseWheel,
    onMouseOver,
    onMouseOut
  }
}
</script>


<style scoped>
.cp-earth{
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}
</style>
